package felix.things.router.modbus.monitor;

import com.ghgande.j2mod.modbus.Modbus;
import com.ghgande.j2mod.modbus.ModbusException;
import com.ghgande.j2mod.modbus.io.AbstractModbusTransport;
import com.ghgande.j2mod.modbus.io.ModbusSerialTransaction;
import com.ghgande.j2mod.modbus.io.ModbusTransaction;
import com.ghgande.j2mod.modbus.msg.*;
import com.ghgande.j2mod.modbus.procimg.InputRegister;
import com.ghgande.j2mod.modbus.procimg.Register;
import com.ghgande.j2mod.modbus.util.BitVector;
import felix.things.router.modbus.config.ModBusConfig;
import felix.things.router.modbus.config.TransportConfig;
import felix.things.router.modbus.mapping.TypeTagMapping;
import felix.things.router.modbus.net.ModbusMasterFactory;
import felix.things.router.modbus.response.ModBusDataProcessor;
import felix.things.router.modbus.response.ModBusResponseProcessor;
import felix.things.router.modbus.transport.RtuTransportConfiguration;
import felix.things.router.modbus.transport.TcpTransportConfiguration;
import felix.things.router.modbus.transport.TransportConfiguration;
import felix.things.router.modbus.transport.UdpTransportConfiguration;
import felix.things.router.modbus.util.ModbusUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.*;
import java.util.concurrent.*;

/**
 * @author Felix
 * @date 2019/11/1
 **/
@Slf4j
@Component
public class ModBusMonitor {
    @Autowired
    private TransportConfig transportConfig;
    @Autowired
    private ModBusConfig modBusConfig;

    @Autowired(required = false)
    private List<ModBusResponseProcessor> modBusResponseProcessors;
    @Autowired(required = false)
    private List<ModBusDataProcessor> modBusDataProcessors;

    private Map<String, ModBusResponseProcessor> responseProcessorMap;
    private Map<String, ModBusDataProcessor> dataProcessorMap;

    private ExecutorService cachedThreadPool = Executors.newCachedThreadPool();


    @PostConstruct
    public void init() {
        if (modBusConfig.isEnable()) {
            log.info("ModBus Router Enabled");
            variableConstructor();
            convertMap();
            createTransport();
        } else {
            log.info("ModBus Router DisEnabled");
        }
    }

    private void variableConstructor() {
        responseProcessorMap = new ConcurrentHashMap<>();
        dataProcessorMap = new ConcurrentHashMap<>();
    }

    private void convertMap() {
        if (modBusResponseProcessors != null) {
            modBusResponseProcessors.forEach(modBusResponseProcessor -> {
                responseProcessorMap.putIfAbsent(modBusResponseProcessor.getTag(), modBusResponseProcessor);
            });
        }
        if (modBusDataProcessors != null) {
            modBusDataProcessors.forEach(modBusDataProcessor -> {
                dataProcessorMap.putIfAbsent(modBusDataProcessor.getTag(), modBusDataProcessor);
            });
        }
    }

    private HashMap<String, Object> processResponse(String tag, ModbusResponse response, int start, int end) {
        log.trace("【{}】:Request: 【{}】\tResponse: 【{}】", tag, response.getHexMessage(), response.getHexMessage());
        ModBusDataProcessor modBusDataProcessor = dataProcessorMap.get(tag);
        if (modBusDataProcessor == null) {
            return null;
        }
        List<TypeTagMapping> mappings = modBusDataProcessor.getMappings();
        if (mappings == null || mappings.size() < start) {
            return null;
        }
        if (mappings.size() < end) {
            end = mappings.size();
        }
        mappings = mappings.subList(start, end);
        if (response instanceof ReadCoilsResponse) {
            ReadCoilsResponse data = (ReadCoilsResponse) response;
            BitVector coils = data.getCoils();
            log.debug("【{}】:Data: 【{}】", tag, coils);
            return ModbusUtils.convertToDataEntry(mappings, coils);
        } else if (response instanceof ReadInputDiscretesResponse) {
            ReadInputDiscretesResponse data = (ReadInputDiscretesResponse) response;
            BitVector coils = data.getDiscretes();
            log.debug("【{}】:Data: 【{}】", tag, coils);
            return ModbusUtils.convertToDataEntry(mappings, coils);
        } else if (response instanceof ReadMultipleRegistersResponse) {
            ReadMultipleRegistersResponse data = (ReadMultipleRegistersResponse) response;
            Register[] values = data.getRegisters();
            log.debug("【{}】:Data: 【{}】", tag, Arrays.toString(values));
            return ModbusUtils.convertToDataEntry(mappings, values, start);
        } else if (response instanceof ReadInputRegistersResponse) {
            ReadInputRegistersResponse data = (ReadInputRegistersResponse) response;
            InputRegister[] values = data.getRegisters();
            log.debug("【{}】:Data: 【{}】", tag, Arrays.toString(values));
            return ModbusUtils.convertToDataEntry(mappings, values, start);
        } else {
            log.warn("ModbusResponse type 【{}】 not support", response.getClass().getSimpleName());
            return null;
        }
    }

    private ModbusRequest createRequest(TransportConfiguration configuration, int start, int count) {
        ModbusRequest request;
        switch (configuration.getFunctionCode()) {
            case Modbus.READ_COILS:
                request = new ReadCoilsRequest(start, count);
                break;
            case Modbus.READ_INPUT_DISCRETES:
                request = new ReadInputDiscretesRequest(start, count);
                break;
            case Modbus.READ_MULTIPLE_REGISTERS:
                request = new ReadMultipleRegistersRequest(start, count);
                break;
            case Modbus.READ_INPUT_REGISTERS:
                request = new ReadInputRegistersRequest(start, count);
                break;
            default:
                request = new IllegalFunctionRequest(configuration.getFunctionCode());
                break;
        }
        request.setUnitID(configuration.getUnit());
        return request;
    }

    private void createTransport() {
        Map<String, TcpTransportConfiguration> tcp = Optional.ofNullable(transportConfig.getTcp()).orElse(new HashMap<>());
        tcp.forEach((s, transportConfiguration) -> {
            startTransport("tcp:" + s, transportConfiguration);
        });
        Map<String, UdpTransportConfiguration> udp = Optional.ofNullable(transportConfig.getUdp()).orElse(new HashMap<>());
        udp.forEach((s, transportConfiguration) -> {
            startTransport("udp:" + s, transportConfiguration);
        });
        Map<String, RtuTransportConfiguration> rtu = Optional.ofNullable(transportConfig.getRtu()).orElse(new HashMap<>());
        rtu.forEach((s, transportConfiguration) -> {
            startTransport("rtu:" + s, transportConfiguration);
        });
    }

    private void startTransport(String s, TransportConfiguration configuration) {
        cachedThreadPool.execute(() -> {
            AbstractModbusTransport modbusTransport = null;
            while (modbusTransport == null) {
                modbusTransport = ModbusMasterFactory.createModbusTransport(configuration);
                if (modbusTransport == null) {
                    log.error("can not connect 【{}】 try next time", s);
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            log.info("connect 【{}】", s);
            startPolling(s, modbusTransport.createTransaction(), configuration);
        });
    }

    /**
     * 多个transaction循环读取数据
     */
    private void startPolling(String tag, ModbusTransaction transaction, TransportConfiguration configuration) {
        Integer count = configuration.getCount();
        log.info("Start polling");
        ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
        executor.scheduleWithFixedDelay(() -> {
                    int group = count / modBusConfig.getMaxRequestSize();
                    int start;
                    int end;
                    int i;
                    for (i = 0; i < group; i++) {
                        start = configuration.getRef() + i * modBusConfig.getMaxRequestSize();
                        end = configuration.getRef() + (i + 1) * modBusConfig.getMaxRequestSize();
                        modbusRequest(tag, transaction, configuration, start, end);
                    }
                    start = configuration.getRef() + i * modBusConfig.getMaxRequestSize();
                    end = configuration.getRef() + count;
                    modbusRequest(tag, transaction, configuration, start, end);
                },
                0, configuration.getRetryInterval(), TimeUnit.MILLISECONDS);
    }

    private void modbusRequest(String tag, ModbusTransaction transaction, TransportConfiguration configuration, int start, int end) {
        ModbusRequest request = createRequest(configuration, start, end - start);
        transaction.setRequest(request);
        request.setHeadless(transaction instanceof ModbusSerialTransaction);
        try {
            transaction.execute();
        } catch (ModbusException x) {
            log.error("【{}】:Request: 【{}】:Execute Error: 【{}】", tag, request.getHexMessage(), x.getMessage());
            try {
                log.error("【{}】: wait for next request【{}】ms", tag, configuration.getRetryInterval());
                Thread.sleep(configuration.getRetryInterval());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return;
        }
        try {
            ModbusResponse response = transaction.getResponse();
            if (response == null) {
                log.warn("【{}】:Request: 【{}】\tNo response to READ HOLDING request", tag, request.getHexMessage());
                return;
            }
            if (response instanceof ExceptionResponse) {
                ExceptionResponse exception = (ExceptionResponse) response;
                log.error("【{}】:Request: 【{}】\t【{}】", tag, request.getHexMessage(), exception.toString());
                return;
            }
            ModBusResponseProcessor modBusResponseProcessor = responseProcessorMap.get(tag);
            if (modBusResponseProcessor != null) {
                response = modBusResponseProcessor.processorResponse(tag, response, start, end);
            }

            HashMap<String, Object> data = processResponse(tag, response, start, end);
            ModBusDataProcessor modBusDataProcessor = dataProcessorMap.get(tag);
            if (modBusDataProcessor != null && data != null) {
                modBusDataProcessor.processorData(tag, data, start, end);
            }
        } catch (Exception e) {
            log.error("【{}】:Unknown Error ", tag, e);
        }
    }
}
